Skip to content

feat(init): add fuzzy search for module selection#1180

Merged
danielroe merged 8 commits intonuxt:mainfrom
onmax:feat/module-autocomplete
Feb 5, 2026
Merged

feat(init): add fuzzy search for module selection#1180
danielroe merged 8 commits intonuxt:mainfrom
onmax:feat/module-autocomplete

Conversation

@onmax
Copy link
Contributor

@onmax onmax commented Jan 11, 2026

Init project
Recording.2026-01-11.185725.mp4
Add module
Recording.2026-01-11.192052.mp4

Summary

  • Add autocomplete search for modules during nuxi init
  • Show ALL compatible modules (not just official)
  • Official modules sorted first
  • Toggle selection with checkmarks (✔/○)
  • Live summary of selected modules
  • Uses fzf for fast fuzzy matching

Probably the UX can be improved, but not sure how, feel free to iterate or suggest improvements 💅

Closes #1174

Test plan

  • Run nuxi init /tmp/test
  • Say yes to "browse and install modules"
  • Type to filter (e.g. "tail" → tailwindcss)
  • Enter to toggle, Esc when done

@onmax onmax requested a review from danielroe as a code owner January 11, 2026 17:56
@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 11, 2026

  • nuxt-cli-playground

    npm i https://pkg.pr.new/create-nuxt@1180
    
    npm i https://pkg.pr.new/nuxi@1180
    
    npm i https://pkg.pr.new/@nuxt/cli@1180
    

commit: 1d25796

@github-actions
Copy link
Contributor

github-actions bot commented Jan 11, 2026

📦 Bundle Size Comparison

📈 nuxi

Metric Base Head Diff
Rendered 3674.40 KB 3699.34 KB +24.94 KB (+0.68%)

📈 nuxt-cli

Metric Base Head Diff
Rendered 137.20 KB 139.00 KB +1.80 KB (+1.31%)

📈 create-nuxt

Metric Base Head Diff
Rendered 1622.52 KB 1646.49 KB +23.97 KB (+1.48%)

@codecov-commenter
Copy link

codecov-commenter commented Jan 11, 2026

Codecov Report

❌ Patch coverage is 61.53846% with 30 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@2555142). Learn more about missing BASE report.

Files with missing lines Patch % Lines
packages/nuxi/src/commands/module/add.ts 16.66% 20 Missing ⚠️
packages/nuxi/src/commands/init.ts 0.00% 10 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1180   +/-   ##
=======================================
  Coverage        ?   25.19%           
=======================================
  Files           ?       90           
  Lines           ?     4910           
  Branches        ?      279           
=======================================
  Hits            ?     1237           
  Misses          ?     3643           
  Partials        ?       30           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq
Copy link

codspeed-hq bot commented Jan 11, 2026

CodSpeed Performance Report

Merging this PR will not alter performance

Comparing onmax:feat/module-autocomplete (1d25796) with main (2555142)

Summary

✅ 2 untouched benchmarks

@onmax onmax force-pushed the feat/module-autocomplete branch from abedffb to ebcafc2 Compare January 11, 2026 18:04
Copy link
Member

@danielroe danielroe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is amazing! 🚀

@coderabbitai
Copy link

coderabbitai bot commented Feb 5, 2026

📝 Walkthrough

Walkthrough

Adds an interactive fuzzy-search module selector and integrates it into the CLI. New file packages/nuxi/src/commands/module/_autocomplete.ts exports AutocompleteOptions, AutocompleteResult, and selectModulesAutocomplete which uses fzf + clack prompts. init and module add commands were updated to use the new autocomplete flow. Unit tests for the autocomplete feature were added at packages/nuxi/test/unit/commands/module/_autocomplete.spec.ts. The runtime dependency "fzf" was added to packages/nuxi and packages/nuxt-cli package.json, and knip.json was updated to ignore fzf for the nuxt-cli workspace.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main feature added: fuzzy search for module selection, which is the primary change throughout the PR.
Description check ✅ Passed The description relates to the changeset by explaining the fuzzy search feature implementation, module sorting, and selection behavior with visual examples.
Linked Issues check ✅ Passed The PR addresses issue #1174 by enabling module installation during project initialization with an interactive fuzzy search interface showing all compatible modules, though not exclusively for Tailwind.
Out of Scope Changes check ✅ Passed All changes relate to adding interactive module selection: dependencies added (fzf), autocomplete utility created, init and add commands modified, tests added, and configuration updated accordingly.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/nuxi/src/commands/init.ts (1)

455-484: ⚠️ Potential issue | 🟡 Minor

Respect explicit cancellation from module autocomplete.

If the user cancels the autocomplete prompt, the flow currently proceeds silently. Consider handling result.cancelled to abort consistently with other prompts.

💡 Suggested adjustment
-          const result = await selectModulesAutocomplete({ modules: allModules })
+          const result = await selectModulesAutocomplete({ modules: allModules })
+          if (result.cancelled) {
+            cancel('Operation cancelled.')
+            process.exit(1)
+          }

           if (result.selected.length > 0) {
             const modules = result.selected
🤖 Fix all issues with AI agents
In `@packages/nuxi/package.json`:
- Line 54: The package.json currently lists "fzf" under devDependencies but it
is imported/used at runtime in
packages/nuxi/src/commands/module/_autocomplete.ts, so move the "fzf" entry from
devDependencies to dependencies in packages/nuxi/package.json (i.e., remove it
from devDependencies and add the same version under dependencies) and then
reinstall/build so the runtime bundle includes it.

In `@packages/nuxi/src/commands/module/add.ts`:
- Around line 88-115: The interactive branch can leave the spinner running on
fetch failures and doesn't handle explicit cancellation from
selectModulesAutocomplete; wrap the Promise.all call that uses fetchModules()
and getNuxtVersion(cwd) in a try/catch (or use .catch) and ensure
spinner().stop() is called in a finally block so the spinner always stops on
error, logging or calling cancel() with the caught error and exiting; after
calling selectModulesAutocomplete({modules: compatibleModules,...}) check
result.cancelled first and call cancel('Selection cancelled.') and exit, then
handle result.selected.length === 0 as the empty-selection case, and keep
references to spinner, fetchModules, getNuxtVersion, selectModulesAutocomplete,
cancel and process.exit when implementing these guards.
🧹 Nitpick comments (1)
packages/nuxi/src/commands/module/_autocomplete.ts (1)

41-61: Cache fuzzy results per search term to avoid O(n²) filtering.

fzf.find(search) is recomputed for each option; a cached Set per search term cuts repeated work on large module lists.

♻️ Proposed refactor
   const fzf = new Fzf(sortedModules, {
     selector: m => `${m.npm} ${m.name} ${m.category}`,
     casing: 'case-insensitive',
     tiebreakers: [byLengthAsc],
   })
+  const matchCache = new Map<string, Set<string>>()

   // Custom filter function using fzf for fuzzy matching
   const filter = (search: string, option: Option<string>): boolean => {
     if (!search)
       return true
-    const results = fzf.find(search)
-    return results.some(r => r.item.npm === option.value)
+    let matches = matchCache.get(search)
+    if (!matches) {
+      matches = new Set(fzf.find(search).map(r => r.item.npm))
+      matchCache.set(search, matches)
+    }
+    return matches.has(option.value)
   }

"defu": "^6.1.4",
"exsolve": "^1.0.8",
"fuse.js": "^7.1.0",
"fzf": "^0.5.2",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if fzf is imported in the mentioned file
cat packages/nuxi/src/commands/module/_autocomplete.ts | head -50

Repository: nuxt/cli

Length of output: 1582


🏁 Script executed:

# Check the build configuration and package.json structure
cat packages/nuxi/package.json | head -80

Repository: nuxt/cli

Length of output: 2002


🏁 Script executed:

# Search for fzf usage in the codebase
rg "fzf" packages/nuxi/src --type ts --type js

Repository: nuxt/cli

Length of output: 515


🏁 Script executed:

# Check for build configuration files
fd -e "tsconfig\|vite\|webpack\|rollup\|build" packages/nuxi/ --type f

Repository: nuxt/cli

Length of output: 413


🏁 Script executed:

# Search for tsdown configuration files
fd -e config -name "*tsdown*" packages/nuxi/
fd . packages/nuxi --maxdepth 2 --type f \( -name "tsdown*" -o -name "build*" -o -name ".tsdownrc*" \)

Repository: nuxt/cli

Length of output: 513


🏁 Script executed:

# Check if there's a dist folder and what it contains
ls -la packages/nuxi/dist 2>/dev/null | head -20 || echo "dist not found or not built"

Repository: nuxt/cli

Length of output: 34


🏁 Script executed:

# Search for external/externalize config in package.json or tsconfig
grep -i "external\|bundle" packages/nuxi/package.json

Repository: nuxt/cli

Length of output: 34


🏁 Script executed:

# Check tsdown documentation/behavior in context of how packages are typically bundled
rg "tsdown|bundle" packages/nuxi --type json --type ts -A 2 | head -30

Repository: nuxt/cli

Length of output: 1693


🏁 Script executed:

# Read the tsdown configuration
cat -n packages/nuxi/tsdown.config.ts

Repository: nuxt/cli

Length of output: 892


🏁 Script executed:

# Also check if there's any external configuration in the config
cat packages/nuxi/tsdown.config.ts | grep -i "external\|dependencies"

Repository: nuxt/cli

Length of output: 34


Move fzf from devDependencies to dependencies.

fzf is imported and used at runtime in the CLI (packages/nuxi/src/commands/module/_autocomplete.ts). The tsdown build configuration does not bundle dependencies by default, meaning consumers who install the package will not receive fzf since it's currently only a devDependency. This will cause the CLI to fail at runtime with a "module not found" error when the autocomplete functionality is invoked.

Proposed fix
{
  "name": "nuxi",
  "type": "module",
  "version": "3.32.0",
  "description": "Nuxt CLI",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/nuxt/cli.git",
    "directory": "packages/nuxi"
  },
  "exports": {
    ".": "./dist/index.mjs",
    "./cli": "./bin/nuxi.mjs"
  },
  "types": "./dist/index.d.ts",
  "bin": {
    "nuxi": "bin/nuxi.mjs",
    "nuxi-ng": "bin/nuxi.mjs",
    "nuxt": "bin/nuxi.mjs",
    "nuxt-cli": "bin/nuxi.mjs"
  },
  "files": [
    "bin",
    "dist"
  ],
  "engines": {
    "node": "^16.10.0 || >=18.0.0"
  },
  "scripts": {
    "build": "tsdown",
    "prepack": "pnpm build",
    "test:dist": "node ./bin/nuxi.mjs info ../../playground"
  },
+  "dependencies": {
+    "fzf": "^0.5.2"
+  },
  "devDependencies": {
    "@bomb.sh/tab": "^0.0.12",
    "@clack/prompts": "1.0.0",
    "@nuxt/kit": "^4.3.0",
    "@nuxt/schema": "^4.3.0",
    "@nuxt/test-utils": "^3.23.0",
    "@types/copy-paste": "^2.1.0",
    "@types/debug": "^4.1.12",
    "@types/node": "^24.10.10",
    "@types/semver": "^7.7.1",
    "c12": "^3.3.3",
    "citty": "^0.2.0",
    "confbox": "^0.2.2",
    "consola": "^3.4.2",
    "copy-paste": "^2.2.0",
    "debug": "^4.4.3",
    "defu": "^6.1.4",
    "exsolve": "^1.0.8",
    "fuse.js": "^7.1.0",
-    "fzf": "^0.5.2",
    "giget": "^3.1.1",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"fzf": "^0.5.2",
"scripts": {
"build": "tsdown",
"prepack": "pnpm build",
"test:dist": "node ./bin/nuxi.mjs info ../../playground"
},
"dependencies": {
"fzf": "^0.5.2"
},
"devDependencies": {
"@bomb.sh/tab": "^0.0.12",
"@clack/prompts": "1.0.0",
"@nuxt/kit": "^4.3.0",
"@nuxt/schema": "^4.3.0",
"@nuxt/test-utils": "^3.23.0",
"@types/copy-paste": "^2.1.0",
"@types/debug": "^4.1.12",
"@types/node": "^24.10.10",
"@types/semver": "^7.7.1",
"c12": "^3.3.3",
"citty": "^0.2.0",
"confbox": "^0.2.2",
"consola": "^3.4.2",
"copy-paste": "^2.2.0",
"debug": "^4.4.3",
"defu": "^6.1.4",
"exsolve": "^1.0.8",
"fuse.js": "^7.1.0",
"giget": "^3.1.1",
🤖 Prompt for AI Agents
In `@packages/nuxi/package.json` at line 54, The package.json currently lists
"fzf" under devDependencies but it is imported/used at runtime in
packages/nuxi/src/commands/module/_autocomplete.ts, so move the "fzf" entry from
devDependencies to dependencies in packages/nuxi/package.json (i.e., remove it
from devDependencies and add the same version under dependencies) and then
reinstall/build so the runtime bundle includes it.

Comment on lines +88 to +115
// If no modules specified, show interactive search
if (modules.length === 0) {
const modulesSpinner = spinner()
modulesSpinner.start('Fetching available modules')

const [allModules, nuxtVersion] = await Promise.all([
fetchModules(),
getNuxtVersion(cwd),
])

const compatibleModules = allModules.filter(m =>
!m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion),
)

modulesSpinner.stop('Modules loaded')

const result = await selectModulesAutocomplete({
modules: compatibleModules,
message: 'Search modules to add (Esc to finish):',
})

if (result.selected.length === 0) {
cancel('No modules selected.')
process.exit(0)
}

modules = result.selected
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle module fetch failures and explicit cancellation in the interactive flow.

A failed fetch will currently throw and leave the spinner running; also explicit cancellation isn’t handled separately from empty selection. Consider adding a guarded fetch and a result.cancelled branch.

🐛 Suggested hardening
     if (modules.length === 0) {
       const modulesSpinner = spinner()
       modulesSpinner.start('Fetching available modules')

-      const [allModules, nuxtVersion] = await Promise.all([
-        fetchModules(),
-        getNuxtVersion(cwd),
-      ])
+      let allModules: NuxtModule[]
+      let nuxtVersion: string
+      try {
+        [allModules, nuxtVersion] = await Promise.all([
+          fetchModules(),
+          getNuxtVersion(cwd),
+        ])
+      }
+      catch (err) {
+        modulesSpinner.stop('Failed to load modules')
+        logger.error(err instanceof Error ? err.message : String(err))
+        process.exit(1)
+      }

       const compatibleModules = allModules.filter(m =>
         !m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion),
       )

       modulesSpinner.stop('Modules loaded')

       const result = await selectModulesAutocomplete({
         modules: compatibleModules,
         message: 'Search modules to add (Esc to finish):',
       })
+      if (result.cancelled) {
+        cancel('Operation cancelled.')
+        process.exit(1)
+      }

       if (result.selected.length === 0) {
         cancel('No modules selected.')
         process.exit(0)
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// If no modules specified, show interactive search
if (modules.length === 0) {
const modulesSpinner = spinner()
modulesSpinner.start('Fetching available modules')
const [allModules, nuxtVersion] = await Promise.all([
fetchModules(),
getNuxtVersion(cwd),
])
const compatibleModules = allModules.filter(m =>
!m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion),
)
modulesSpinner.stop('Modules loaded')
const result = await selectModulesAutocomplete({
modules: compatibleModules,
message: 'Search modules to add (Esc to finish):',
})
if (result.selected.length === 0) {
cancel('No modules selected.')
process.exit(0)
}
modules = result.selected
}
// If no modules specified, show interactive search
if (modules.length === 0) {
const modulesSpinner = spinner()
modulesSpinner.start('Fetching available modules')
let allModules: NuxtModule[]
let nuxtVersion: string
try {
[allModules, nuxtVersion] = await Promise.all([
fetchModules(),
getNuxtVersion(cwd),
])
}
catch (err) {
modulesSpinner.stop('Failed to load modules')
logger.error(err instanceof Error ? err.message : String(err))
process.exit(1)
}
const compatibleModules = allModules.filter(m =>
!m.compatibility.nuxt || checkNuxtCompatibility(m, nuxtVersion),
)
modulesSpinner.stop('Modules loaded')
const result = await selectModulesAutocomplete({
modules: compatibleModules,
message: 'Search modules to add (Esc to finish):',
})
if (result.cancelled) {
cancel('Operation cancelled.')
process.exit(1)
}
if (result.selected.length === 0) {
cancel('No modules selected.')
process.exit(0)
}
modules = result.selected
}
🤖 Prompt for AI Agents
In `@packages/nuxi/src/commands/module/add.ts` around lines 88 - 115, The
interactive branch can leave the spinner running on fetch failures and doesn't
handle explicit cancellation from selectModulesAutocomplete; wrap the
Promise.all call that uses fetchModules() and getNuxtVersion(cwd) in a try/catch
(or use .catch) and ensure spinner().stop() is called in a finally block so the
spinner always stops on error, logging or calling cancel() with the caught error
and exiting; after calling selectModulesAutocomplete({modules:
compatibleModules,...}) check result.cancelled first and call cancel('Selection
cancelled.') and exit, then handle result.selected.length === 0 as the
empty-selection case, and keep references to spinner, fetchModules,
getNuxtVersion, selectModulesAutocomplete, cancel and process.exit when
implementing these guards.

@danielroe danielroe merged commit e4b7fff into nuxt:main Feb 5, 2026
12 of 13 checks passed
@github-actions github-actions bot mentioned this pull request Feb 5, 2026
@onmax onmax deleted the feat/module-autocomplete branch February 5, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Install Tailwind option in Nuxt project installer

3 participants